<?php

/**
 * This file is part of the Propel package.
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license    MIT License
 */

require_once dirname(__FILE__) . '/../../model/AppData.php';
require_once dirname(__FILE__) . '/../../exception/SchemaException.php';

/**
 * A class that is used to parse an input xml schema file and creates an AppData
 * PHP object.
 *
 * @author     Hans Lellelid <hans@xmpl.org> (Propel)
 * @author     Leon Messerschmidt <leon@opticode.co.za> (Torque)
 * @author     Jason van Zyl <jvanzyl@apache.org> (Torque)
 * @author     Martin Poeschl <mpoeschl@marmot.at> (Torque)
 * @author     Daniel Rall <dlr@collab.net> (Torque)
 * @version    $Revision: 1965 $
 * @package    propel.generator.builder.util
 */
class XmlToAppData
{

	/** enables debug output */
	const DEBUG = false;

	private $app;
	private $currDB;
	private $currTable;
	private $currColumn;
	private $currFK;
	private $currIndex;
	private $currUnique;
	private $currValidator;
	private $currBehavior;
	private $currVendorObject;

	private $isForReferenceOnly;
	private $currentPackage;
	private $currentXmlFile;
	private $defaultPackage;

	private $encoding;

	/** two-dimensional array,
		first dimension is for schemas(key is the path to the schema file),
		second is for tags within the schema */
	private $schemasTagsStack = array();

	/**
	 * Creates a new instance for the specified database type.
	 *
	 * @param      PropelPlatformInterface $defaultPlatform The default database platform for the application.
	 * @param      string $defaultPackage the default PHP package used for the om
	 * @param      string $encoding The database encoding.
	 */
	public function __construct(PropelPlatformInterface $defaultPlatform = null, $defaultPackage = null, $encoding = 'iso-8859-1')
	{
		$this->app = new AppData($defaultPlatform);
		$this->defaultPackage = $defaultPackage;
		$this->firstPass = true;
		$this->encoding = $encoding;
	}

	/**
	 * Set the AppData generator configuration
	 *
	 * @param GeneratorConfig $generatorConfig
	 */
	public function setGeneratorConfig(GeneratorConfig $generatorConfig)
	{
		$this->app->setGeneratorConfig($generatorConfig);
	}

	/**
	 * Parses a XML input file and returns a newly created and
	 * populated AppData structure.
	 *
	 * @param      string $xmlFile The input file to parse.
	 * @return     AppData populated by <code>xmlFile</code>.
	 */
	public function parseFile($xmlFile)
	{
		// we don't want infinite recursion
		if ($this->isAlreadyParsed($xmlFile)) {
			return;
		}
		
		return $this->parseString(file_get_contents($xmlFile), $xmlFile);
	}

	/**
	 * Parses a XML input string and returns a newly created and
	 * populated AppData structure.
	 *
	 * @param      string $xmlString The input string to parse.
	 * @param      string $xmlFile The input file name.
	 * @return     AppData populated by <code>xmlFile</code>.
	 */
	public function parseString($xmlString, $xmlFile = null)
	{
		// we don't want infinite recursion
		if ($this->isAlreadyParsed($xmlFile)) {
			return;
		}
		// store current schema file path
		$this->schemasTagsStack[$xmlFile] = array();
		$this->currentXmlFile = $xmlFile;

		$parser = xml_parser_create();
		xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
		xml_set_object($parser, $this);
		xml_set_element_handler($parser, 'startElement', 'endElement');
		if (!xml_parse($parser, $xmlString)) {
			throw new Exception(sprintf("XML error: %s at line %d",
				xml_error_string(xml_get_error_code($parser)),
				xml_get_current_line_number($parser))
			);
		}
		xml_parser_free($parser);
		
		array_pop($this->schemasTagsStack);

		return $this->app;
	}
	
	/**
	 * Handles opening elements of the xml file.
	 *
	 * @param      string $uri
	 * @param      string $localName The local name (without prefix), or the empty string if
	 *		 Namespace processing is not being performed.
	 * @param      string $rawName The qualified name (with prefix), or the empty string if
	 *		 qualified names are not available.
	 * @param      string $attributes The specified or defaulted attributes
	 */
	public function startElement($parser, $name, $attributes)
	{
	  $parentTag = $this->peekCurrentSchemaTag();

	  if ($parentTag === false) {

				switch($name) {
					case "database":
						if ($this->isExternalSchema()) {
							$this->currentPackage = @$attributes["package"];
							if ($this->currentPackage === null) {
								$this->currentPackage = $this->defaultPackage;
							}
						} else {
							$this->currDB = $this->app->addDatabase($attributes);
						}
					break;

					default:
						$this->_throwInvalidTagException($parser, $name);
				}

		} elseif  ($parentTag == "database") {

			switch($name) {

				case "external-schema":
					$xmlFile = @$attributes["filename"];

					// "referenceOnly" attribute is valid in the main schema XML file only,
					// and it's ignored in the nested external-schemas
					if (!$this->isExternalSchema()) {
						$isForRefOnly = @$attributes["referenceOnly"];
						$this->isForReferenceOnly = ($isForRefOnly !== null ? (strtolower($isForRefOnly) === "true") : true); // defaults to TRUE
					}

					if ($xmlFile{0} != '/') {
						$xmlFile = realpath(dirname($this->currentXmlFile) . DIRECTORY_SEPARATOR . $xmlFile);
						if (!file_exists($xmlFile)) {
							throw new SchemaException(sprintf('Unknown include external "%s"', $xmlFile));
						}
					}

					$this->parseFile($xmlFile);
				break;

  		  case "domain":
				  $this->currDB->addDomain($attributes);
			  break;

				case "table":
					$this->currTable = $this->currDB->addTable($attributes);
					if ($this->isExternalSchema()) {
						$this->currTable->setForReferenceOnly($this->isForReferenceOnly);
						$this->currTable->setPackage($this->currentPackage);
					}
				break;

				case "vendor":
					$this->currVendorObject = $this->currDB->addVendorInfo($attributes);
				break;

				case "behavior":
				  $this->currBehavior = $this->currDB->addBehavior($attributes);
				break;

				default:
					$this->_throwInvalidTagException($parser, $name);
			}

		} elseif  ($parentTag == "table") {

			switch($name) {
				case "column":
					$this->currColumn = $this->currTable->addColumn($attributes);
				break;

				case "foreign-key":
					$this->currFK = $this->currTable->addForeignKey($attributes);
				break;

				case "index":
					$this->currIndex = $this->currTable->addIndex($attributes);
				break;

				case "unique":
					$this->currUnique = $this->currTable->addUnique($attributes);
				break;

				case "vendor":
					$this->currVendorObject = $this->currTable->addVendorInfo($attributes);
				break;

	  		case "validator":
				  $this->currValidator = $this->currTable->addValidator($attributes);
	  		break;

	  		case "id-method-parameter":
					$this->currTable->addIdMethodParameter($attributes);
				break;
        
				case "behavior":
				  $this->currBehavior = $this->currTable->addBehavior($attributes);
				break;
				
				default:
					$this->_throwInvalidTagException($parser, $name);
			}

		} elseif  ($parentTag == "column") {

			switch($name) {
				case "inheritance":
					$this->currColumn->addInheritance($attributes);
				break;

				case "vendor":
					$this->currVendorObject = $this->currColumn->addVendorInfo($attributes);
				break;

				default:
					$this->_throwInvalidTagException($parser, $name);
			}

		} elseif ($parentTag == "foreign-key") {

			switch($name) {
				case "reference":
					$this->currFK->addReference($attributes);
				break;

				case "vendor":
					$this->currVendorObject = $this->currUnique->addVendorInfo($attributes);
				break;

				default:
					$this->_throwInvalidTagException($parser, $name);
			}

		} elseif  ($parentTag == "index") {

			switch($name) {
				case "index-column":
					$this->currIndex->addColumn($attributes);
				break;

				case "vendor":
					$this->currVendorObject = $this->currIndex->addVendorInfo($attributes);
				break;

				default:
					$this->_throwInvalidTagException($parser, $name);
			}

		} elseif ($parentTag == "unique") {

			switch($name) {
				case "unique-column":
					$this->currUnique->addColumn($attributes);
				break;

				case "vendor":
					$this->currVendorObject = $this->currUnique->addVendorInfo($attributes);
				break;

				default:
					$this->_throwInvalidTagException($parser, $name);
			}
		} elseif ($parentTag == "behavior") {

			switch($name) {
				case "parameter":
					$this->currBehavior->addParameter($attributes);
				break;

				default:
					$this->_throwInvalidTagException($parser, $name);
			}
		} elseif ($parentTag == "validator") {
			switch($name) {
				case "rule":
					$this->currValidator->addRule($attributes);
				break;
				default:
					$this->_throwInvalidTagException($parser, $name);
			}
		} elseif ($parentTag == "vendor") {

			switch($name) {
				case "parameter":
					$this->currVendorObject->addParameter($attributes);
				break;
				default:
					$this->_throwInvalidTagException($parser, $name);
			}

		} else {
			// it must be an invalid tag
			$this->_throwInvalidTagException($parser, $name);
		}

		$this->pushCurrentSchemaTag($name);
	}

	function _throwInvalidTagException($parser, $tag_name)
	{
		$location = '';
		if ($this->currentXmlFile !== null) {
			$location .= sprintf('file %s,', $this->currentXmlFile);
		}
		$location .= sprintf('line %d', xml_get_current_line_number($parser));
		if ($col = xml_get_current_column_number($parser)) {
			$location .= sprintf(', column %d', $col);
		}
		throw new SchemaException(sprintf('Unexpected tag <%s> in %s', $tag_name, $location));
	}

	/**
	 * Handles closing elements of the xml file.
	 *
	 * @param      uri
	 * @param      localName The local name (without prefix), or the empty string if
	 *		 Namespace processing is not being performed.
	 * @param      rawName The qualified name (with prefix), or the empty string if
	 *		 qualified names are not available.
	 */
	public function endElement($parser, $name)
	{
		if (self::DEBUG) {
			print("endElement(" . $name . ") called\n");
		}

		$this->popCurrentSchemaTag();
	}
	
	protected function peekCurrentSchemaTag()
	{
		$keys = array_keys($this->schemasTagsStack);
		return end($this->schemasTagsStack[end($keys)]);
	}

	protected function popCurrentSchemaTag()
	{
		$keys = array_keys($this->schemasTagsStack);
		array_pop($this->schemasTagsStack[end($keys)]);
	}

	protected function pushCurrentSchemaTag($tag)
	{
		$keys = array_keys($this->schemasTagsStack);
		$this->schemasTagsStack[end($keys)][] = $tag;
	}

	protected function isExternalSchema()
	{
		return count($this->schemasTagsStack) > 1;
	}

	protected function isAlreadyParsed($filePath)
	{
		return isset($this->schemasTagsStack[$filePath]);
	}
}
